Car Price Prediction - Exploratory data analysis.
Вступление¶
EDA (разведочный анализ данных) - это процесс изучения основных свойств данных, нахождения в них закономерностей, аномалий и различных распределений, построений моделей с помощью визуализации.
Известно, что не существует единой последовательности операций для выполнения анализа, но есть базовые пункты, которые необходимо выполнить.
Анализ проводится над выбранном в kaggle dataset - см. ссылки далее.
Информация об dataset.¶
Dataset - Car Price Prediction Challenge - является набором данных об различных проданных автомобилях с их параметрами.
Например:
- Manufacturer (Производитель)
- Leather interior (Кожаный салон)
- Fuel type (Тип топлива)
и т.д ...
Всего таких параметров - 15.
Ссылка на оригинальный dataset - https://www.kaggle.com/datasets/deepcontractor/car-price-prediction-challenge?resource=download
Ссылка на dataset, сохраненный в Google Drive - https://docs.google.com/spreadsheets/d/1PMhtD3LqyCzlZMEh-8aDPxre0wPw8v0U/edit?usp=drive_link&ouid=100105970921534140705&rtpof=true&sd=true
Цель (target) EDA.¶
Целью EDA является - выявление ключевых факторов (features), влияющих на стоимость.
Также передо мной стоит цель соответствовать следующим метрикам (как минимум первым двум):

Этапы EDA.¶
1) Загрузка данных с сайта и сохранения их в директории для дальнейшего взаимодействия¶
[!IMPORTANT]
Всегда активируем окружение как рассказано в файле README.md.
Не забываем добавить в нашего окружение (Conda+Poetry) необходимые зависимости:
- pandas
- numpy
- seaborn
- matplotlib
- openpyxl
- plotly
В первую очередь, так как датасет не сохранен в директории проекта, необходимо создать директорию data в корне. А после выполнить в созданную директорию загрузку сырых данных в формате xlsx.
Выполним подключение основных библиотек для проведения EDA.
import os
import requests
import pandas as pd
import numpy as np
import seaborn as sns
import matplotlib.pyplot as plt
import matplotlib.colors as mc
import plotly.express as px
import plotly.io as pio
import plotly.graph_objects as go
from dotenv import load_dotenv
pio.renderers.default = 'notebook'
%matplotlib inline
load_dotenv()
file_id = os.getenv("FILE_ID")
Выполним подключение кастомной палитры для графиков в EDA.
color = "#f1faee" # проставляем цвет фона - АКТУАЛЬНЫЙ f1faee
palette = plt.cm.OrRd(
np.linspace(0.5, 1.0, 12)
) # настраиваем палитру OrRd, убирая самые светлые оттенки
translate_into_hex = [
f"#{int(r*255):02x}{int(g*255):02x}{int(b*255):02x}" for r, g, b, a in palette
] # переводим палитру в HEX формат для применения в настройках Matplotlib, Seaborn, Plotly
plt.rcParams.update(
{
"font.family": "Arial",
"font.size": 12,
"figure.figsize": (14, 6),
"figure.facecolor": f"{color}",
"axes.facecolor": f"{color}",
"grid.color": "#f0f0f0",
"axes.prop_cycle": plt.cycler(color=translate_into_hex),
}
)
sns.set_theme(
style="whitegrid",
palette=translate_into_hex,
font="Arial",
font_scale=1.0,
rc={
"figure.figsize": (14, 6),
"figure.facecolor": f"{color}",
"axes.facecolor": f"{color}",
"grid.color": "#f0f0f0",
},
)
custom_template = go.layout.Template()
custom_template.layout.colorway = translate_into_hex
custom_template.layout.font = {"family": "Arial", "size": 12}
custom_template.layout.title = {"x": 0.5, "font": {"size": 16, "weight": "bold"}}
custom_template.layout.paper_bgcolor = f"{color}"
custom_template.layout.plot_bgcolor = f"{color}"
pio.templates["my_custom"] = custom_template
pio.templates.default = "my_custom"
Здесь выполняем загрузку dataset с GD или чтение уже имеющегося.
file_url = f"https://drive.google.com/uc?id={file_id}"
pd.set_option("display.max_columns", 20) # убирает ограничения отображения dataset
pd.set_option("display.max_rows", 150)
data_dir = "../data/EDA"
if not os.path.exists(data_dir):
os.makedirs(data_dir)
data_path = os.path.join(data_dir, "data_car.xlsx")
if os.path.isfile(data_path): # если файл есть в data - читаем, если нет - скачиваем
df = pd.read_excel(data_path)
df = df.replace("-", pd.NA) # убираем все ложные пропуски на NaN
print(f"{'-'*60}")
print("Dataset прочитан из директории data...")
print(f"{'-'*60}")
else:
response = requests.get(file_url)
f_path = os.path.join(data_dir, "data_car.xlsx")
with open(f_path, "wb") as f:
f.write(response.content)
df = pd.read_excel(f_path)
df = df.replace("-", pd.NA)
print(f"{'-'*60}")
print("Dataset загружен в директорию data и прочитан...")
print(f"{'-'*60}")
------------------------------------------------------------ Dataset загружен в директорию data и прочитан... ------------------------------------------------------------
2) Первичный осмотр данных¶
После чтения/загрузки dataset из директории/в директорию необходимо осмотреть полученные данные и провести первичную оценку.
Поэтому выведем 10 первых и 10 последних значений dataset. А также проверим количество признаков и элементов.
df.head(10)
| ID | Price | Levy | Manufacturer | Model | Prod. year | Category | Leather interior | Fuel type | Engine volume | Mileage | Cylinders | Gear box type | Drive wheels | Doors | Wheel | Color | Airbags | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | 45654403 | 13328 | 1399 | LEXUS | RX 450 | 2010 | Jeep | Yes | Hybrid | 3.5 | 186005 km | 6.0 | Automatic | 4x4 | 04-May | Left wheel | Silver | 12 |
| 1 | 44731507 | 16621 | 1018 | CHEVROLET | Equinox | 2011 | Jeep | No | Petrol | 3 | 192000 km | 6.0 | Tiptronic | 4x4 | 04-May | Left wheel | Black | 8 |
| 2 | 45774419 | 8467 | <NA> | HONDA | FIT | 2006 | Hatchback | No | Petrol | 1.3 | 200000 km | 4.0 | Variator | Front | 04-May | Right-hand drive | Black | 2 |
| 3 | 45769185 | 3607 | 862 | FORD | Escape | 2011 | Jeep | Yes | Hybrid | 2.5 | 168966 km | 4.0 | Automatic | 4x4 | 04-May | Left wheel | White | 0 |
| 4 | 45809263 | 11726 | 446 | HONDA | FIT | 2014 | Hatchback | Yes | Petrol | 1.3 | 91901 km | 4.0 | Automatic | Front | 04-May | Left wheel | Silver | 4 |
| 5 | 45802912 | 39493 | 891 | HYUNDAI | Santa FE | 2016 | Jeep | Yes | Diesel | 2 | 160931 km | 4.0 | Automatic | Front | 04-May | Left wheel | White | 4 |
| 6 | 45656768 | 1803 | 761 | TOYOTA | Prius | 2010 | Hatchback | Yes | Hybrid | 1.8 | 258909 km | 4.0 | Automatic | Front | 04-May | Left wheel | White | 12 |
| 7 | 45816158 | 549 | 751 | HYUNDAI | Sonata | 2013 | Sedan | Yes | Petrol | 2.4 | 216118 km | 4.0 | Automatic | Front | 04-May | Left wheel | Grey | 12 |
| 8 | 45641395 | 1098 | 394 | TOYOTA | Camry | 2014 | Sedan | Yes | Hybrid | 2.5 | 398069 km | 4.0 | Automatic | Front | 04-May | Left wheel | Black | 12 |
| 9 | 45756839 | 26657 | <NA> | LEXUS | RX 350 | 2007 | Jeep | Yes | Petrol | 3.5 | 128500 km | 6.0 | Automatic | 4x4 | 04-May | Left wheel | Silver | 12 |
df.tail(10)
| ID | Price | Levy | Manufacturer | Model | Prod. year | Category | Leather interior | Fuel type | Engine volume | Mileage | Cylinders | Gear box type | Drive wheels | Doors | Wheel | Color | Airbags | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 19227 | 45769427 | 29793 | 1053 | MERCEDES-BENZ | E 350 | 2014 | Sedan | Yes | Diesel | 3.5 | 219030 km | 6.0 | Automatic | 4x4 | 04-May | Left wheel | Black | 12 |
| 19228 | 45773726 | 706 | 1850 | MERCEDES-BENZ | E 350 | 2008 | Sedan | Yes | Diesel | 3.5 | 122874 km | 6.0 | Automatic | Rear | 04-May | Left wheel | Black | 12 |
| 19229 | 39977395 | 50 | <NA> | TOYOTA | Prius | 2008 | Hatchback | No | Hybrid | 1.5 | 150000 km | 4.0 | Automatic | Front | 04-May | Left wheel | Silver | 6 |
| 19230 | 45760891 | 470 | 645 | TOYOTA | Prius | 2011 | Hatchback | Yes | Hybrid | 1.8 | 307325 km | 4.0 | Automatic | Front | 04-May | Left wheel | Silver | 12 |
| 19231 | 45772306 | 5802 | 1055 | MERCEDES-BENZ | E 350 | 2013 | Sedan | Yes | Diesel | 3.5 | 107800 km | 6.0 | Automatic | Rear | 04-May | Left wheel | Grey | 12 |
| 19232 | 45798355 | 8467 | <NA> | MERCEDES-BENZ | CLK 200 | 1999 | Coupe | Yes | CNG | 2.0 Turbo | 300000 km | 4.0 | Manual | Rear | 02-Mar | Left wheel | Silver | 5 |
| 19233 | 45778856 | 15681 | 831 | HYUNDAI | Sonata | 2011 | Sedan | Yes | Petrol | 2.4 | 161600 km | 4.0 | Tiptronic | Front | 04-May | Left wheel | Red | 8 |
| 19234 | 45804997 | 26108 | 836 | HYUNDAI | Tucson | 2010 | Jeep | Yes | Diesel | 2 | 116365 km | 4.0 | Automatic | Front | 04-May | Left wheel | Grey | 4 |
| 19235 | 45793526 | 5331 | 1288 | CHEVROLET | Captiva | 2007 | Jeep | Yes | Diesel | 2 | 51258 km | 4.0 | Automatic | Front | 04-May | Left wheel | Black | 4 |
| 19236 | 45813273 | 470 | 753 | HYUNDAI | Sonata | 2012 | Sedan | Yes | Hybrid | 2.4 | 186923 km | 4.0 | Automatic | Front | 04-May | Left wheel | White | 12 |
df.shape
(19237, 18)
Предлагаю исследовать в каких типах данных сохранились 18 признаков.
df.dtypes
ID int64 Price int64 Levy object Manufacturer object Model object Prod. year int64 Category object Leather interior object Fuel type object Engine volume object Mileage object Cylinders float64 Gear box type object Drive wheels object Doors object Wheel object Color object Airbags int64 dtype: object
3) Операции над данными.¶
После осмотра данных необходимо выполнить определенные действия для дальнейшего удобства анализа.
Например - решение проблемы пустых значений, дубликатов и выбросов, удаление ненужных для текущей target признаков и приведение типов
3.1) Удаление признаков¶
Так как наша цель заключается в ранжировании признаков по их влиянию на ценообразование, я предлагаю удалить следующие признаки:
- cylinders - так как основной акцент на ценообразование будет от типа двигателя, а не от количества цилиндров.
- drive wheels - опять таки минимальный эффект на ценообразование. И кроме того тип привода завязан на моделе авто и его типа.
- color - минимальное влияния на цену автомобиля.
- airbags - минимальное влияние на цену автомобиля.
df = df.drop(["Cylinders", "Drive wheels", "Color", "Airbags"], axis=1)
df.head(5)
| ID | Price | Levy | Manufacturer | Model | Prod. year | Category | Leather interior | Fuel type | Engine volume | Mileage | Gear box type | Doors | Wheel | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | 45654403 | 13328 | 1399 | LEXUS | RX 450 | 2010 | Jeep | Yes | Hybrid | 3.5 | 186005 km | Automatic | 04-May | Left wheel |
| 1 | 44731507 | 16621 | 1018 | CHEVROLET | Equinox | 2011 | Jeep | No | Petrol | 3 | 192000 km | Tiptronic | 04-May | Left wheel |
| 2 | 45774419 | 8467 | <NA> | HONDA | FIT | 2006 | Hatchback | No | Petrol | 1.3 | 200000 km | Variator | 04-May | Right-hand drive |
| 3 | 45769185 | 3607 | 862 | FORD | Escape | 2011 | Jeep | Yes | Hybrid | 2.5 | 168966 km | Automatic | 04-May | Left wheel |
| 4 | 45809263 | 11726 | 446 | HONDA | FIT | 2014 | Hatchback | Yes | Petrol | 1.3 | 91901 km | Automatic | 04-May | Left wheel |
3.2) Проверка категориальных признаков¶
Типы данных могут быть числовыми и категориальными. Поэтому следует рассмотреть их точнее и в последствии привести их к нужному типу.
df.select_dtypes(exclude="number").head()
| Levy | Manufacturer | Model | Category | Leather interior | Fuel type | Engine volume | Mileage | Gear box type | Doors | Wheel | |
|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | 1399 | LEXUS | RX 450 | Jeep | Yes | Hybrid | 3.5 | 186005 km | Automatic | 04-May | Left wheel |
| 1 | 1018 | CHEVROLET | Equinox | Jeep | No | Petrol | 3 | 192000 km | Tiptronic | 04-May | Left wheel |
| 2 | <NA> | HONDA | FIT | Hatchback | No | Petrol | 1.3 | 200000 km | Variator | 04-May | Right-hand drive |
| 3 | 862 | FORD | Escape | Jeep | Yes | Hybrid | 2.5 | 168966 km | Automatic | 04-May | Left wheel |
| 4 | 446 | HONDA | FIT | Hatchback | Yes | Petrol | 1.3 | 91901 km | Automatic | 04-May | Left wheel |
Как мы видим Levy и Mileage скорее числовые признаки, нежели нечисловые. В Levy имеются пропуски, а в Mileage приписка km. Давайте это исправим.
Посмотрим на количество в Levy пустых значений.
df.isna().sum()
ID 0 Price 0 Levy 5819 Manufacturer 0 Model 0 Prod. year 0 Category 0 Leather interior 0 Fuel type 0 Engine volume 0 Mileage 0 Gear box type 0 Doors 0 Wheel 0 dtype: int64
Визуальное отображение пропусков:
plt.figure(figsize=(12, 6))
plt.imshow(df.isna(), aspect="auto", interpolation="nearest", cmap="gray")
plt.title("Visual display of passes\n", fontsize=16, fontweight="bold")
plt.xlabel("Column Number")
plt.ylabel("Sample Number")
Text(0, 0.5, 'Sample Number')
Можно увидеть количество пропусков в признаке Levy - очень большое. И при этом признак, предполагается, одним из важнейших, так как содержит налог на автомобиль.
Следовательно, лучшим выходом будет выполнить заполнение по медиане.
df["Levy"] = df["Levy"].astype(
"Int16"
) # данный тип данных позволяет преобразовать признак с пропусками
df["Levy"] = df["Levy"].fillna(df["Levy"].median())
df.isna().sum()
ID 0 Price 0 Levy 0 Manufacturer 0 Model 0 Prod. year 0 Category 0 Leather interior 0 Fuel type 0 Engine volume 0 Mileage 0 Gear box type 0 Doors 0 Wheel 0 dtype: int64
Далее разберем признак Mileage, который необходимо привести в числовой тип, при этом убрав приписку km.
df["Mileage"] = df["Mileage"].str.replace(" km", "", regex=False)
df["Mileage"].head(10)
0 186005 1 192000 2 200000 3 168966 4 91901 5 160931 6 258909 7 216118 8 398069 9 128500 Name: Mileage, dtype: object
df["Mileage"] = df["Mileage"].astype("int32")
df["Mileage"].head(10)
0 186005 1 192000 2 200000 3 168966 4 91901 5 160931 6 258909 7 216118 8 398069 9 128500 Name: Mileage, dtype: int32
Еще раз выведем категориальные признаки, чтобы проверить не упустили ли мы что-то.
df.select_dtypes(exclude="number").head()
| Manufacturer | Model | Category | Leather interior | Fuel type | Engine volume | Gear box type | Doors | Wheel | |
|---|---|---|---|---|---|---|---|---|---|
| 0 | LEXUS | RX 450 | Jeep | Yes | Hybrid | 3.5 | Automatic | 04-May | Left wheel |
| 1 | CHEVROLET | Equinox | Jeep | No | Petrol | 3 | Tiptronic | 04-May | Left wheel |
| 2 | HONDA | FIT | Hatchback | No | Petrol | 1.3 | Variator | 04-May | Right-hand drive |
| 3 | FORD | Escape | Jeep | Yes | Hybrid | 2.5 | Automatic | 04-May | Left wheel |
| 4 | HONDA | FIT | Hatchback | Yes | Petrol | 1.3 | Automatic | 04-May | Left wheel |
Признак Engine volume оставляем нечисловым, так как помимо объема двигателя в признаке указывается - с турбонаддувом двигатель или нет
3.3) Проверка числовых признаков¶
Разберем числовые признаки dataset. Кроме того дополнительно проверим нахождения тут измененных признаков из пункта выше.
df.select_dtypes(include="number").head()
| ID | Price | Levy | Prod. year | Mileage | |
|---|---|---|---|---|---|
| 0 | 45654403 | 13328 | 1399 | 2010 | 186005 |
| 1 | 44731507 | 16621 | 1018 | 2011 | 192000 |
| 2 | 45774419 | 8467 | 781 | 2006 | 200000 |
| 3 | 45769185 | 3607 | 862 | 2011 | 168966 |
| 4 | 45809263 | 11726 | 446 | 2014 | 91901 |
Все хорошо, теперь можно рассмотреть количество уникальных значений в каждом из признаков. Это поможет точнее выбрать тип для приведения.
uniq_val = df.select_dtypes(include="number").nunique().sort_values()
fig = px.bar(
x=uniq_val.index,
y=uniq_val.values,
labels={"x": "Features", "y": "Count"},
# template="seaborn",
)
fig.update_layout(
xaxis_tickangle=0,
showlegend=False,
title="Unique values in each feature\n",
)
fig.update_traces(texttemplate="%{y}", textposition="outside")
fig.show()
3.4) Проверка дубликатов¶
Так как мы хотим добиться уникальности записей, то необходимо провести над нашими данными проверку дубликатов по их ID.
df.shape
(19237, 14)
duplic_rows_df = df[df.duplicated()]
duplic_rows_df.shape
(313, 14)
Их оказалось не так много, учитывая весь объем датасета. Поэтому мы можем их смело удалить.
df = df.drop_duplicates()
df.shape
(18924, 14)
3.5) Переименование признаков¶
Анализируя данные мы пришли к выводу, что необходимо переименовать некоторые признаки, так как они мало отражают содержащиеся в них сведения.
Все переименования будут указны также в файле NAMING.txt в директории docs.
Перед этим выведем все названия.
df.head()
| ID | Price | Levy | Manufacturer | Model | Prod. year | Category | Leather interior | Fuel type | Engine volume | Mileage | Gear box type | Doors | Wheel | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | 45654403 | 13328 | 1399 | LEXUS | RX 450 | 2010 | Jeep | Yes | Hybrid | 3.5 | 186005 | Automatic | 04-May | Left wheel |
| 1 | 44731507 | 16621 | 1018 | CHEVROLET | Equinox | 2011 | Jeep | No | Petrol | 3 | 192000 | Tiptronic | 04-May | Left wheel |
| 2 | 45774419 | 8467 | 781 | HONDA | FIT | 2006 | Hatchback | No | Petrol | 1.3 | 200000 | Variator | 04-May | Right-hand drive |
| 3 | 45769185 | 3607 | 862 | FORD | Escape | 2011 | Jeep | Yes | Hybrid | 2.5 | 168966 | Automatic | 04-May | Left wheel |
| 4 | 45809263 | 11726 | 446 | HONDA | FIT | 2014 | Hatchback | Yes | Petrol | 1.3 | 91901 | Automatic | 04-May | Left wheel |
df = df.rename(
columns={
"Levy": "Tax",
"Prod. year": "Release_year",
"Category": "Car_type",
"Gear box type": "Transmission_type",
}
)
df.head()
| ID | Price | Tax | Manufacturer | Model | Release_year | Car_type | Leather interior | Fuel type | Engine volume | Mileage | Transmission_type | Doors | Wheel | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | 45654403 | 13328 | 1399 | LEXUS | RX 450 | 2010 | Jeep | Yes | Hybrid | 3.5 | 186005 | Automatic | 04-May | Left wheel |
| 1 | 44731507 | 16621 | 1018 | CHEVROLET | Equinox | 2011 | Jeep | No | Petrol | 3 | 192000 | Tiptronic | 04-May | Left wheel |
| 2 | 45774419 | 8467 | 781 | HONDA | FIT | 2006 | Hatchback | No | Petrol | 1.3 | 200000 | Variator | 04-May | Right-hand drive |
| 3 | 45769185 | 3607 | 862 | FORD | Escape | 2011 | Jeep | Yes | Hybrid | 2.5 | 168966 | Automatic | 04-May | Left wheel |
| 4 | 45809263 | 11726 | 446 | HONDA | FIT | 2014 | Hatchback | Yes | Petrol | 1.3 | 91901 | Automatic | 04-May | Left wheel |
3.6) Обнаружение и обработка выбросов в числовых признаках¶
Важно предупредить момент выбросов и ошибочных огромных значений в числовых параметрах. Особенно перед приведением типов.
Такие выбросы будут негативно влиять на наш конечный вывод, что приведет к некорректному решению поставленной цели.
Вызовем для отображения снова числовые параметры.
df.select_dtypes(include="number").head()
| ID | Price | Tax | Release_year | Mileage | |
|---|---|---|---|---|---|
| 0 | 45654403 | 13328 | 1399 | 2010 | 186005 |
| 1 | 44731507 | 16621 | 1018 | 2011 | 192000 |
| 2 | 45774419 | 8467 | 781 | 2006 | 200000 |
| 3 | 45769185 | 3607 | 862 | 2011 | 168966 |
| 4 | 45809263 | 11726 | 446 | 2014 | 91901 |
df["Price"].nlargest(30)
16983 26307500 8541 872946 1225 627220 5008 308906 9367 297930 14839 297930 7749 288521 10759 260296 5840 254024 15283 250574 7283 228935 2283 219527 7353 216391 1145 194438 13328 193184 4722 175622 2768 172486 2912 172486 6468 172486 9248 172486 7718 167781 13351 163077 4044 156805 11941 156805 15413 153669 10423 150533 17868 150533 7997 147397 18881 147397 13265 144261 Name: Price, dtype: int64
df["Tax"].nlargest(30)
115 11714 18984 11714 18957 11706 17117 7536 3994 7063 2159 7058 5529 5908 5367 5877 17767 5681 2323 5679 14676 5679 8160 5666 2357 5603 17777 5603 3639 5332 19048 4860 17495 4741 9222 4736 1571 4508 8887 4283 14892 4057 11413 3989 13973 3965 16119 3910 10955 3894 18487 3811 14979 3743 6582 3739 16695 3699 18543 3571 Name: Tax, dtype: Int16
df["Release_year"].nlargest(30)
956 2020 979 2020 1225 2020 1626 2020 2133 2020 3641 2020 4049 2020 4077 2020 4590 2020 5291 2020 5735 2020 6421 2020 7276 2020 7390 2020 7529 2020 9138 2020 9261 2020 9663 2020 10382 2020 10559 2020 10732 2020 11109 2020 11920 2020 12038 2020 12430 2020 12480 2020 12556 2020 12593 2020 12976 2020 13016 2020 Name: Release_year, dtype: int64
df["Mileage"].nlargest(30)
2278 2147483647 6157 2147483647 11901 2147483647 12734 2147483647 15347 2147483647 15393 2147483647 19167 2147483647 17582 1777777778 7724 1234567899 9524 1111111111 19199 1111111111 5456 999999999 10667 999999999 12591 999999999 12904 999999999 16586 999999999 985 777777777 15364 222222222 18477 111111111 11144 58008888 1404 55556665 17206 40000000 11472 23000000 8695 20000000 9542 20000000 8824 18065445 18673 15000000 4823 12648846 1892 11111111 8486 11111111 Name: Mileage, dtype: int32
Как видно - проблемы есть. А конкретно в признаках Price, Tax, Mileage.
Это необходимо решать. Начнем с признака Price - приблизим до первых пяти максимальных значений.
df["Price"].nlargest(5)
16983 26307500 8541 872946 1225 627220 5008 308906 9367 297930 Name: Price, dtype: int64
Давайте посмотрим какие автомобили имеют такой ценник.
df[df["Price"].isin([26307500, 872946, 627220])]
| ID | Price | Tax | Manufacturer | Model | Release_year | Car_type | Leather interior | Fuel type | Engine volume | Mileage | Transmission_type | Doors | Wheel | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 1225 | 45795524 | 627220 | 781 | MERCEDES-BENZ | G 65 AMG 63AMG | 2020 | Jeep | Yes | Petrol | 6.3 Turbo | 0 | Tiptronic | 04-May | Left wheel |
| 8541 | 45761204 | 872946 | 2067 | LAMBORGHINI | Urus | 2019 | Universal | Yes | Petrol | 4 | 2531 | Tiptronic | 04-May | Left wheel |
| 16983 | 45812886 | 26307500 | 781 | OPEL | Combo | 1999 | Goods wagon | No | Diesel | 1.7 | 99999 | Manual | 02-Mar | Left wheel |
Мы нашли и проанализировали выбросы и первые два автомобиля - имеют адекватную цену. А вот третий - Opel Combo не может стоить такую сумму. Это необходимо исправить.
df["Price"] = df["Price"].mask(df["Price"] > 900000, df["Price"].median())
df["Price"].nlargest(5)
8541 872946 1225 627220 5008 308906 9367 297930 14839 297930 Name: Price, dtype: int64
Что мы сделали?
Так как в признаке мы обнаружили и определили единичный выброс с суммой, которая невозможна для того типа автомобиля (цены указаны в долларах).
Поэтому было принято решение заменить этот выброс на медианное значение.
Ниже снова выведем максимальные значения признака Price:
df["Price"].nlargest(30)
8541 872946 1225 627220 5008 308906 9367 297930 14839 297930 7749 288521 10759 260296 5840 254024 15283 250574 7283 228935 2283 219527 7353 216391 1145 194438 13328 193184 4722 175622 2768 172486 2912 172486 6468 172486 9248 172486 7718 167781 13351 163077 4044 156805 11941 156805 15413 153669 10423 150533 17868 150533 7997 147397 18881 147397 13265 144261 3006 141124 Name: Price, dtype: int64
Теперь проанализируем Tax похожим методом, как и Price.
df["Tax"].nlargest(10)
115 11714 18984 11714 18957 11706 17117 7536 3994 7063 2159 7058 5529 5908 5367 5877 17767 5681 2323 5679 Name: Tax, dtype: Int16
Опять посмотрим на автомобили:
df[df["Tax"].isin([11714, 11706, 7536, 7063, 7058])]
| ID | Price | Tax | Manufacturer | Model | Release_year | Car_type | Leather interior | Fuel type | Engine volume | Mileage | Transmission_type | Doors | Wheel | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 115 | 45534351 | 11917 | 11714 | MERCEDES-BENZ | E 500 AMG | 2003 | Sedan | Yes | Petrol | 5 | 150000 | Tiptronic | 04-May | Right-hand drive |
| 2159 | 45804871 | 10349 | 7058 | SUBARU | Legacy | 2005 | Sedan | Yes | Petrol | 3 | 147000 | Tiptronic | 04-May | Right-hand drive |
| 3994 | 45782188 | 13172 | 7063 | TOYOTA | Alphard | 2003 | Minivan | Yes | LPG | 3 | 190000 | Automatic | 04-May | Right-hand drive |
| 17117 | 45781442 | 7213 | 7536 | MITSUBISHI | Pajero | 2000 | Jeep | Yes | CNG | 3.2 | 210000 | Automatic | 04-May | Right-hand drive |
| 18957 | 44674964 | 14740 | 11706 | MERCEDES-BENZ | E 500 AVG | 2005 | Sedan | Yes | Petrol | 5 | 56000 | Tiptronic | 04-May | Right-hand drive |
| 18984 | 45221191 | 11917 | 11714 | MERCEDES-BENZ | E 500 | 2003 | Sedan | Yes | Petrol | 5 | 150000 | Tiptronic | 04-May | Right-hand drive |
Просмотрев автомобили с такими налогами стало понятно - выбросов в признаке НЕТ!
Теперь проверим признак Mileage:
df["Mileage"].nlargest(10)
2278 2147483647 6157 2147483647 11901 2147483647 12734 2147483647 15347 2147483647 15393 2147483647 19167 2147483647 17582 1777777778 7724 1234567899 9524 1111111111 Name: Mileage, dtype: int32
В этом признаке явно что-то аномальное (магические числа). Проверим с помощью межквартильного размаха(IQR).
Определение IQR - это статистическая мера, показывающая разброс средних 50% данных, отсекая крайние значения.
Q1_Mil = df["Mileage"].quantile(0.25)
Q3_Mil = df["Mileage"].quantile(0.75)
IQR_Mil = Q3_Mil - Q1_Mil
up_bound_Mil = Q3_Mil + 1.5 * IQR_Mil
down_bound_Mil = Q1_Mil - 1.5 * IQR_Mil
errors = df[df["Mileage"] > up_bound_Mil]
errors_type_2 = df[df["Mileage"] < down_bound_Mil]
len(errors) # количество записей-выбросов выше верхней границы
635
len(errors_type_2) # количество записей-выбросов ниже нижней границы
0
Делаем вывод, что 635 значений - у нас точно являются выбросами или некорректными значениями.
Так как у нас почти 19000 строчек, то принимаем решение по удалению этих выбросов.
df = df[
df["Mileage"] <= up_bound_Mil
] # оставляем в Mileage только те значения, которые попали до верхней границы
df["Mileage"].nlargest(10)
1086 367053 7259 367000 14709 367000 4961 366869 8948 366869 9125 366869 10944 366869 15132 366869 16628 366869 9840 365810 Name: Mileage, dtype: int32
plt.figure(figsize=(12, 6))
sns.boxplot(x=df["Mileage"])
plt.title("Emissions in <Mileage> after delete\n", fontsize=16, fontweight="bold")
plt.show()
Вывод - мы задетектировали выбросы в числовых признаках и их убрали.
3.7) Приведение типов и сохранение в формате .parquet¶
Дальнейший анализ предполагает точный результат, поэтому я хотел бы использовать максимально обработанные и скорректированные данные. И в быстро работающем формате .parquet.
Выведем признаки и их типы:
df.dtypes
ID int64 Price int64 Tax Int16 Manufacturer object Model object Release_year int64 Car_type object Leather interior object Fuel type object Engine volume object Mileage int32 Transmission_type object Doors object Wheel object dtype: object
Выполним приведение типов:
df["ID"] = df["ID"].astype("int32")
df["Price"] = df["Price"].astype("int32")
df["Manufacturer"] = df["Manufacturer"].astype("category")
df["Model"] = df["Model"].astype("category")
df["Tax"] = df["Tax"].astype("int32")
df["Release_year"] = df["Release_year"].astype("int16")
df["Car_type"] = df["Car_type"].astype("category")
df["Have a leather interior?"] = (
df["Leather interior"].map({"No": 0, "Yes": 1}).astype("bool")
) # добавляем к оригинальному признаку булевый признак.
df["Car have a left wheel?"] = (
df["Wheel"].map({"Right-hand drive": 0, "Left wheel": 1}).astype("bool")
)
df["Fuel type"] = df["Fuel type"].astype("category")
df["Transmission_type"] = df["Transmission_type"].astype("category")
df["Doors"] = df["Doors"].astype("category")
df[["Leather interior", "Wheel"]] = df[
["Have a leather interior?", "Car have a left wheel?"]
] # заменяем оригинал на булевый.
df = df.drop(
columns=["Have a leather interior?", "Car have a left wheel?"]
) # удаляем дополнительные признаки.
df.rename(
columns={"Leather interior": "Have a leather interior?"}, inplace=True
) # переименовываем.
df.rename(columns={"Wheel": "Car have a left wheel?"}, inplace=True)
df.dtypes
ID int32 Price int32 Tax int32 Manufacturer category Model category Release_year int16 Car_type category Have a leather interior? bool Fuel type category Engine volume object Mileage int32 Transmission_type category Doors category Car have a left wheel? bool dtype: object
Мы привели типы и теперь сохраним наш новый рабочий dataset в формате .parquet
fi_path = os.path.join(data_dir, "data_car.parquet") # путь до файла dataset
df.to_parquet(fi_path) # сохраняем dataset в .parquet
df_par = pd.read_parquet(fi_path)
df_par.head()
| ID | Price | Tax | Manufacturer | Model | Release_year | Car_type | Have a leather interior? | Fuel type | Engine volume | Mileage | Transmission_type | Doors | Car have a left wheel? | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | 45654403 | 13328 | 1399 | LEXUS | RX 450 | 2010 | Jeep | True | Hybrid | 3.5 | 186005 | Automatic | 04-May | True |
| 1 | 44731507 | 16621 | 1018 | CHEVROLET | Equinox | 2011 | Jeep | False | Petrol | 3 | 192000 | Tiptronic | 04-May | True |
| 2 | 45774419 | 8467 | 781 | HONDA | FIT | 2006 | Hatchback | False | Petrol | 1.3 | 200000 | Variator | 04-May | False |
| 3 | 45769185 | 3607 | 862 | FORD | Escape | 2011 | Jeep | True | Hybrid | 2.5 | 168966 | Automatic | 04-May | True |
| 4 | 45809263 | 11726 | 446 | HONDA | FIT | 2014 | Hatchback | True | Petrol | 1.3 | 91901 | Automatic | 04-May | True |
completeness = df_par.count() / len(df_par)
uniqueness = df_par["ID"].nunique() / len(
df_par["ID"]
) # так как этот параметр напрямую отвечает за уникальность значений в признаках.
print(completeness)
print(f"{'-'*60}")
print(f"ID = {uniqueness}")
ID 1.0 Price 1.0 Tax 1.0 Manufacturer 1.0 Model 1.0 Release_year 1.0 Car_type 1.0 Have a leather interior? 1.0 Fuel type 1.0 Engine volume 1.0 Mileage 1.0 Transmission_type 1.0 Doors 1.0 Car have a left wheel? 1.0 dtype: float64 ------------------------------------------------------------ ID = 1.0
5) Валидация данных¶
Проведем валидацию корректности данных с помощью assert.
Если проверки будут успешными - то переходим к графикам и диаграммам.
assert (df_par["Price"].dtype == "int32") and ((df_par["Price"] >= 0).all()), print(
"ERROR in Price"
)
assert (df_par["Tax"].dtype == "int32") and ((df_par["Tax"] >= 0).all()), print(
"ERROR in Tax"
)
assert (
(df_par["Release_year"].dtype == "int16")
and ((df_par["Release_year"] >= 1900).all())
and ((df_par["Release_year"] <= 2100).all())
), print("ERROR in Release_year")
assert (df_par["Have a leather interior?"].dtype == "bool") and (
(df_par["Have a leather interior?"].isin([True, False]).all())
), print("ERROR in Have a leather interior?")
assert (df_par["Mileage"].dtype == "int32") and ((df_par["Mileage"] >= 0).all()), print(
"ERROR in Mileage"
)
assert (df_par["Car have a left wheel?"].dtype == "bool") and (
(df_par["Car have a left wheel?"].isin([True, False]).all())
), print("ERROR in Car have a left wheel?")
Валидация числовых и булевых признаков - прошла успешно!
6) Анализы данных для решения задачи¶
6.1) Анализ распределения целевой переменной - Price¶
Повторно озвучиваем цель - провести ранжирование признаков по влиянию на ценообразования автомобиля.
Следовательно основной целевой переменной мы берем признак Price. Выведем гистограмму.
fig = px.histogram(
df_par,
x="Price",
nbins=50,
# template="seaborn",
)
fig.update_layout(
xaxis_tickangle=0,
showlegend=False,
title="Price distribution",
)
fig.show()
Как мы обсуждали выше - цена выше 600000 долларов у нескольких автомобилей - это нормальная цена.
6.2) Построение корреляционной карты и точечных графиков для коррелирующих признаков¶
Для определения корреляций между признаками воспользуемся методами heatmap и scatterplot.
num_df_par = df_par.select_dtypes(include="number").columns
plt.figure(figsize=(10, 8))
sns.heatmap(df_par[num_df_par].corr(), annot=True, cmap="OrRd")
plt.title("The correlation matrix\n", fontsize=16, fontweight="bold")
plt.show()
top_corr_ft = (
df_par[num_df_par].corr()["Price"].abs().sort_values(ascending=False).index[1:4]
)
for features in top_corr_ft:
sns.scatterplot(data=df_par, x=features, y="Price")
plt.title(f"<Price> vs <{features}>\n", fontsize=16, fontweight="bold")
plt.show()
Предлагаю рассмотреть распределения признаков Release_year, Mileage, Tax:
num_cols = ["Release_year", "Mileage", "Tax"] # числовые признаки
fig, axes = plt.subplots(2, 2, figsize=(12, 6))
fig.suptitle("Distribution of numerical features\n", fontsize=16, fontweight="bold")
axes_flat = axes.flat # преобразование массива 2х2 в массив 1:4 для перебора элементов
for i, col in enumerate(num_cols):
sns.histplot(data=df_par, x=col, ax=axes_flat[i], bins=50, kde=True)
axes_flat[i].set_title(f"Distribution <{col}>\n")
axes_flat[i].tick_params(axis="x", rotation=0)
for i in range(len(num_cols), len(axes_flat)):
axes_flat[i].set_visible(False)
plt.tight_layout()
plt.show()
Промежуточный вывод - мы получили наши точечные графики корреляции признаков с основным признаком Price.
Сразу заметно, что эти графики представляют собой своеобразное "облако из точек" и в некоторых ситуациях, например с признаками Mileage и Pelease_year, можно заметить что-то похожее на, в первом случае нисходящую прямую линию, а во втором случае восходящую прямую линию.
Что может говорить нам следующее:
- С увеличением пробега - цена автомобиля падает.
- Чем новее выпущен автомобиль - тем выше его цена.
Но не стоит забыть, что в основном это "облака из точек", т.е интерпретируя это показывает на достаточно слабую зависимость.
Также гипотеза о слабой зависимости Price от числовых параметров подтверждается коррелляционной матрицей. Так как максимальное число в ней, из положительных естественно, это 0,27 - что, исходя из определения коэффициента корреляции Пирсона, говорит нам о слабой положительной корреляции.
Кроме того из распределений сделаем следующие выводы:
- В основном в нем представлены автомобили выпуска 2000-2018 годов.
- Среднем значение пробега - примерно от 100000 до 150000 км.
- Средний налог на авто - 500 долларов
А эти данные указывают на то, что датасет создавался на основе реальных величин и реальных показателей в автомобильной промышленности.
Теперь проведем проверку категориальных признаков:
!!!Примечание!!! - выведем все признаки кроме Model, так как последний из-за количества уникальных наименований - не отображается.
plt.rcParams["font.family"] = "DejaVu Sans"
plt.rcParams["font.sans-serif"] = ["DejaVu Sans", "Arial Unicode MS", "Liberation Sans"]
cat_ft = df_par.select_dtypes(exclude="number").columns
feature_manufacturer = cat_ft[0]
plt.figure(figsize=(25, 10))
unique_categories = df_par[
feature_manufacturer
].unique() # получаем уникальные значения в признаке
for i, category in enumerate(unique_categories):
category_data = df_par[
df_par[feature_manufacturer] == category
] # содержит строки соответствующие уникальному значению
if len(category_data) > 0:
min_price = category_data[
"Price"
].min() # нормализует цены из долларов в диапазон от 0 до 1
max_price = category_data["Price"].max()
if max_price > min_price:
normalized_prices = (category_data["Price"] - min_price) / (
max_price - min_price
)
else:
normalized_prices = pd.Series([0.5] * len(category_data))
color_indices = (normalized_prices * (len(translate_into_hex) - 1)).astype(
int
) # в соответствии с нормализацией красит каждое уникальное значение
colors_for_category = [translate_into_hex[idx] for idx in color_indices]
x_pos = i + np.random.normal(
0, 0.001, size=len(category_data)
) # специально заданные разброс точек, для лучшего их отображения
# x_pos = [i] * len(category_data)
plt.scatter(
x_pos, category_data["Price"], c=colors_for_category, alpha=0.7, s=30
)
plt.xticks(range(len(unique_categories)), unique_categories, rotation=90)
plt.title(f"<Price> vs <{feature_manufacturer}>", fontsize=16, fontweight="bold")
plt.tight_layout()
plt.show()
feature_car_type = cat_ft[2]
plt.figure(figsize=(25, 10))
unique_categories = df_par[feature_car_type].unique()
for i, category in enumerate(unique_categories):
category_data = df_par[df_par[feature_car_type] == category]
if len(category_data) > 0:
min_price = category_data["Price"].min()
max_price = category_data["Price"].max()
if max_price > min_price:
normalized_prices = (category_data["Price"] - min_price) / (
max_price - min_price
)
else:
normalized_prices = pd.Series([0.5] * len(category_data))
color_indices = (normalized_prices * (len(translate_into_hex) - 1)).astype(int)
colors_for_category = [translate_into_hex[idx] for idx in color_indices]
x_pos = i + np.random.normal(0, 0.001, size=len(category_data))
plt.scatter(
x_pos, category_data["Price"], c=colors_for_category, alpha=0.7, s=30
)
plt.xticks(range(len(unique_categories)), unique_categories, rotation=90)
plt.title(f"<Price> vs <{feature_car_type}>", fontsize=16, fontweight="bold")
plt.tight_layout()
plt.show()
feature_leather = cat_ft[3]
plt.figure(figsize=(25, 10))
unique_categories = df_par[feature_leather].unique()
for i, category in enumerate(unique_categories):
category_data = df_par[df_par[feature_leather] == category]
if len(category_data) > 0:
min_price = category_data["Price"].min()
max_price = category_data["Price"].max()
if max_price > min_price:
normalized_prices = (category_data["Price"] - min_price) / (
max_price - min_price
)
else:
normalized_prices = pd.Series([0.5] * len(category_data))
color_indices = (normalized_prices * (len(translate_into_hex) - 1)).astype(int)
colors_for_category = [translate_into_hex[idx] for idx in color_indices]
x_pos = i + np.random.normal(0, 0.001, size=len(category_data))
# x_pos = [i] * len(category_data)
plt.scatter(
x_pos, category_data["Price"], c=colors_for_category, alpha=0.7, s=30
)
plt.xticks(range(len(unique_categories)), unique_categories, rotation=90)
plt.title(f"<Price> vs <{feature_leather}>", fontsize=16, fontweight="bold")
plt.tight_layout()
plt.show()
feature_fuel_type = cat_ft[4]
plt.figure(figsize=(25, 10))
unique_categories = df_par[feature_fuel_type].unique()
for i, category in enumerate(unique_categories):
category_data = df_par[df_par[feature_fuel_type] == category]
if len(category_data) > 0:
min_price = category_data["Price"].min()
max_price = category_data["Price"].max()
if max_price > min_price:
normalized_prices = (category_data["Price"] - min_price) / (
max_price - min_price
)
else:
normalized_prices = pd.Series([0.5] * len(category_data))
color_indices = (normalized_prices * (len(translate_into_hex) - 1)).astype(int)
colors_for_category = [translate_into_hex[idx] for idx in color_indices]
x_pos = i + np.random.normal(0, 0.001, size=len(category_data))
# x_pos = [i] * len(category_data)
plt.scatter(
x_pos, category_data["Price"], c=colors_for_category, alpha=0.7, s=30
)
plt.xticks(range(len(unique_categories)), unique_categories, rotation=90)
plt.title(f"<Price> vs <{feature_fuel_type}>", fontsize=16, fontweight="bold")
plt.tight_layout()
plt.show()
feature_engine_vol = cat_ft[5]
plt.figure(figsize=(25, 10))
unique_categories = df_par[feature_engine_vol].unique()
for i, category in enumerate(unique_categories):
category_data = df_par[df_par[feature_engine_vol] == category]
if len(category_data) > 0:
min_price = category_data["Price"].min()
max_price = category_data["Price"].max()
if max_price > min_price:
normalized_prices = (category_data["Price"] - min_price) / (
max_price - min_price
)
else:
normalized_prices = pd.Series([0.5] * len(category_data))
color_indices = (normalized_prices * (len(translate_into_hex) - 1)).astype(int)
colors_for_category = [translate_into_hex[idx] for idx in color_indices]
x_pos = i + np.random.normal(0, 0.001, size=len(category_data))
# x_pos = [i] * len(category_data)
plt.scatter(
x_pos, category_data["Price"], c=colors_for_category, alpha=0.7, s=30
)
plt.xticks(range(len(unique_categories)), unique_categories, rotation=90)
plt.title(f"<Price> vs <{feature_engine_vol}>", fontsize=16, fontweight="bold")
plt.tight_layout()
plt.show()
feature_transmission = cat_ft[6]
plt.figure(figsize=(25, 10))
unique_categories = df_par[feature_transmission].unique()
for i, category in enumerate(unique_categories):
category_data = df_par[df_par[feature_transmission] == category]
if len(category_data) > 0:
min_price = category_data["Price"].min()
max_price = category_data["Price"].max()
if max_price > min_price:
normalized_prices = (category_data["Price"] - min_price) / (
max_price - min_price
)
else:
normalized_prices = pd.Series([0.5] * len(category_data))
color_indices = (normalized_prices * (len(translate_into_hex) - 1)).astype(int)
colors_for_category = [translate_into_hex[idx] for idx in color_indices]
x_pos = i + np.random.normal(0, 0.001, size=len(category_data))
# x_pos = [i] * len(category_data)
plt.scatter(
x_pos, category_data["Price"], c=colors_for_category, alpha=0.7, s=30
)
plt.xticks(range(len(unique_categories)), unique_categories, rotation=90)
plt.title(f"<Price> vs <{feature_transmission}>", fontsize=16, fontweight="bold")
plt.tight_layout()
plt.show()
feature_doors = cat_ft[7]
plt.figure(figsize=(25, 10))
unique_categories = df_par[feature_doors].unique()
for i, category in enumerate(unique_categories):
category_data = df_par[df_par[feature_doors] == category]
if len(category_data) > 0:
min_price = category_data["Price"].min()
max_price = category_data["Price"].max()
if max_price > min_price:
normalized_prices = (category_data["Price"] - min_price) / (
max_price - min_price
)
else:
normalized_prices = pd.Series([0.5] * len(category_data))
color_indices = (normalized_prices * (len(translate_into_hex) - 1)).astype(int)
colors_for_category = [translate_into_hex[idx] for idx in color_indices]
x_pos = i + np.random.normal(0, 0.001, size=len(category_data))
# x_pos = [i] * len(category_data)
plt.scatter(
x_pos, category_data["Price"], c=colors_for_category, alpha=0.7, s=30
)
plt.xticks(range(len(unique_categories)), unique_categories, rotation=90)
plt.title(f"<Price> vs <{feature_doors}>", fontsize=16, fontweight="bold")
plt.tight_layout()
plt.show()
feature_wheel = cat_ft[8]
plt.figure(figsize=(25, 10))
unique_categories = df_par[feature_wheel].unique()
for i, category in enumerate(unique_categories):
category_data = df_par[df_par[feature_wheel] == category]
if len(category_data) > 0:
min_price = category_data["Price"].min()
max_price = category_data["Price"].max()
if max_price > min_price:
normalized_prices = (category_data["Price"] - min_price) / (
max_price - min_price
)
else:
normalized_prices = pd.Series([0.5] * len(category_data))
color_indices = (normalized_prices * (len(translate_into_hex) - 1)).astype(int)
colors_for_category = [translate_into_hex[idx] for idx in color_indices]
x_pos = i + np.random.normal(0, 0.001, size=len(category_data))
# x_pos = [i] * len(category_data)
plt.scatter(
x_pos, category_data["Price"], c=colors_for_category, alpha=0.7, s=30
)
plt.xticks(range(len(unique_categories)), unique_categories, rotation=90)
plt.title(f"<Price> vs <{feature_wheel}>", fontsize=16, fontweight="bold")
plt.tight_layout()
plt.show()
К сожалению в связи с огромным количеством уникальных значений в признаке Model взять информацию с графика не представляется возможным. Но какие выводы можно сделать по оставшимся:
Промежуточный вывод - благодаря анализу с помощью scatterplot стало известно, что большинство автомобилей в dataset - находятся в достаточно дешевом сегменте. Есть редкие исключения в виде дорогих, но там большую роль играет бренд.
Также, как я писал и ранее - облака точек могут соответствовать тому, что у нас имеется минимальная корреляция признаков на признак Price. Это надо проверить дальше.
Вернем шрифт на первоначальный.
plt.rcParams["font.family"] = "sans-serif"
plt.rcParams["font.sans-serif"] = ["Arial", "DejaVu Sans", "Liberation Sans"]
Далее, поиск в интернете и подсказки от AI, направили меня на поиск модели для создания ранжирования признаков по влиянию на цену.
Так была найдена библиотека scikit-learn откуда взята модель RandomForestRegressor.
Код ниже это процесс работы данной модели.
from sklearn.ensemble import RandomForestRegressor
ft = [
col for col in df_par.columns if col not in ["Price", "ID"]
] # исключаем ID и Price как признаки из анализа
X = df_par[ft].copy() # на ось X ставятся наши признаки
y = df_par["Price"]
X_encoded = X.copy()
for col in X_encoded.select_dtypes(exclude="number").columns:
X_encoded[col] = (
X_encoded[col].astype("category").cat.codes
) # перевод категориальных признаков в числа
model = RandomForestRegressor(
random_state=10, n_estimators=100
) # cама модель, где n_estimators - количество деревьев решений(каждое анализирует по-своему), random_state - только для воспроизводимости результатов (может быть любым)
model.fit(X_encoded, y)
feature_importance = pd.DataFrame(
{"feature": ft, "importance": model.feature_importances_}
).sort_values(
"importance", ascending=False
) # сортировка по важности
plt.figure(figsize=(12, 6))
sns.barplot(
data=feature_importance.head(12),
x="importance",
y="feature",
hue="feature",
palette="OrRd_r",
legend=False,
)
plt.title("Top 12 significant features for car prices", fontsize=16, fontweight="bold")
plt.xlabel(
"The importance of the feature",
)
plt.ylabel("")
plt.tight_layout()
plt.show()
# Выводим таблицу с результатами
print("Ranking of features by importance:")
print("=" * 50)
for i, row in feature_importance.iterrows():
print(f"{i+1:2d}. {row['feature']:25} | importance: {row['importance']:.4f}")
Ranking of features by importance: ================================================== 8. Engine volume | importance: 0.2355 4. Release_year | importance: 0.2153 9. Mileage | importance: 0.1403 3. Model | importance: 0.0956 1. Tax | importance: 0.0767 10. Transmission_type | importance: 0.0659 7. Fuel type | importance: 0.0588 5. Car_type | importance: 0.0471 2. Manufacturer | importance: 0.0436 6. Have a leather interior? | importance: 0.0125 11. Doors | importance: 0.0067 12. Car have a left wheel? | importance: 0.0020
Рассмотрим также корреляцию основных числовых признаков с помощью 3D scatterplot.
Как мы выяснили основными будут - Release year и Mileage.
if len(top_corr_ft) >= 2:
fig = px.scatter_3d(
df_par,
x=top_corr_ft[0],
y=top_corr_ft[1],
z="Price",
color="Price",
title=f"3D scatter: <{top_corr_ft[0]}> vs <{top_corr_ft[1]}> vs <Price>",
# template="seaborn",
color_continuous_scale="OrRd_r",
)
fig.show()
Промежуточный вывод - чем новее автомобиль и чем меньше у него пробег, тем формируемая цена - выше.
Как и требовалось доказать. Но, на графике также видны 2 точки, выбивающиеся из картины по причине сильного воздействия на цену другого признака - Model
7) Итоги¶
Это было сложно и местами непонятно, но мы добрались до конца анализа данных!
Итог - задача была решена. Из графика barplot мы видим, что наибольшее влияние на стоимость автомобиля оказывает два признака Engine_volume и Release_year. Далее все идет по убыванию.
Какой вывод сделал лично я? - есть ощущение, что данные были синтетическими, так как в них на предыдущих этапах были странные выбросы - скорее похожие на ошибки, которые создавал человек. Кроме того присутствовали магические числа в некоторых признаках, а в признаке Tax и только там были пустые значения.
Все это наводит на мысли об искуственности данных
Но при этом же топ-3 признака в итоговой модели показывают на близкие к реальности факты:
- Чем старше автомобиль, тем он дешевле (исключая старые ретро-автомобили, которые не укладываются в такую модель).
- Чем больше у авто объем двигателя (а соответственно и мощность), тем он дороже. Причем это фактор №1 исходя из ранжирования.
- Чем меньше у автомобиля километраж, тем он дороже.
Что меня удивило:
- Удивил факт того, что тип автомобиля, производитель и модель - являются не самыми главными факторами в цене автомобиля. Но стоит учитывать, что модель и производитель решают среднюю цену, ибо Lamborgini и UAZ их медиана цены находится в разных местах.
- Также удивило, что тип руля оказывает наименьшее влияния на цену. Хотя при покупке машины из-за рубежа, особенно с Японии, если у машины правый руль, то и стоит она дешевле.
ВЫВОД - в большинстве своем анализ показал реальную картину формирования стоимости автомобиля, но есть подозрения в том, что взятый dataset с kaggle имеет искусственное происхождение.